package com.jun.common.redis.utils;

import lombok.extern.slf4j.Slf4j;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;

import java.util.Collections;
import java.util.Timer;
import java.util.TimerTask;
import java.util.UUID;
import java.util.concurrent.TimeUnit;

/**
 * @author yuejun.li
 * @date 2024/7/19 14:52:21
 * 分布式锁工具
 */
@Slf4j
public class RedisLockUtil {

    private StringRedisTemplate redisTemplate;

    private static final String LOCK_PREFIX = "redis:lock:";

    private String uuid;

    private Timer timer = new Timer();

    //自动续期
    private long expireTime = 30L;

    private static final DefaultRedisScript<Boolean> TRY_LOCK_SCRIPT;
    private static final DefaultRedisScript<Long> UN_LOCK_SCRIPT;
    private static final DefaultRedisScript<Boolean> RENEW_EXPIRE_SCRIPT;

    public RedisLockUtil(StringRedisTemplate redisTemplate) {
        this.redisTemplate = redisTemplate;
        this.uuid = UUID.randomUUID().toString();
    }

    //饿汉式加载配置lua脚本
    static {
        TRY_LOCK_SCRIPT = new DefaultRedisScript<>();
        UN_LOCK_SCRIPT = new DefaultRedisScript<>();
        RENEW_EXPIRE_SCRIPT = new DefaultRedisScript<>();

        TRY_LOCK_SCRIPT.setLocation(new ClassPathResource("lua/tryLock.lua"));
        TRY_LOCK_SCRIPT.setResultType(Boolean.class);
        UN_LOCK_SCRIPT.setLocation(new ClassPathResource("lua/unLock.lua"));
        UN_LOCK_SCRIPT.setResultType(Long.class);
        RENEW_EXPIRE_SCRIPT.setLocation(new ClassPathResource("lua/renewExpire.lua"));
        RENEW_EXPIRE_SCRIPT.setResultType(Boolean.class);
    }


    /**
     * 基于sentx实现 获取锁
     * @param key 锁名称
     * @param expireTime 锁释放时间 （-1为看门狗机制）
     * @param timeUnit 时间单位
     * @return true-获取锁成功 | false-获取锁失败
     */
    public boolean tryLock(String key, long expireTime, TimeUnit timeUnit){
        key = LOCK_PREFIX.concat(key);
        Boolean lockFlag = false;
        if(expireTime != -1){
            lockFlag = redisTemplate.opsForValue().setIfAbsent(key, this.uuid, expireTime, timeUnit);
        }else{
            lockFlag = redisTemplate.opsForValue().setIfAbsent(key, this.uuid);
        }
        return Boolean.TRUE.equals(lockFlag);
    }


    /**
     * 基于sentx实现 释放锁
     * @param key 锁名称
     */
    public void unLock(String key){
        key = LOCK_PREFIX.concat(key);
        String value = redisTemplate.opsForValue().get(key);
        if(this.uuid.equals(value)){
            redisTemplate.delete(key);
        }
    }

    /**
     * 基于lua实现 获取锁
     * @param key 锁名称
     * @param isSpin true-自旋锁|false-不自旋
     */
    public void luaTryLock(String key,boolean isSpin) throws InterruptedException {
        this.luaTryLock(key,-1, TimeUnit.SECONDS, isSpin);
    }

    /**
     * 基于lua实现 获取锁
     * @param key 锁名称
     * @param expireTime 锁释放时间 （-1为看门狗机制）
     * @param timeUnit 时间单位
     * @param isSpin true-自旋锁|false-不自旋
     */
    public void luaTryLock(String key, long expireTime, TimeUnit timeUnit,boolean isSpin) throws InterruptedException {
        if(expireTime != -1L){
            this.expireTime = timeUnit.toSeconds(expireTime);
        }

        key = LOCK_PREFIX.concat(key);
        String hashKey = getId();
        Boolean flag = redisTemplate.execute(TRY_LOCK_SCRIPT, Collections.singletonList(key), hashKey, String.valueOf(this.expireTime));
        if(isSpin){
            //自旋锁
            while (Boolean.FALSE.equals(flag)){
                TimeUnit.MILLISECONDS.sleep(100);
            }
        }else{
            if(Boolean.FALSE.equals(flag)){
                throw new RuntimeException("系统繁忙");
            }
        }

        if(expireTime == -1L){
            //加锁成功，返回之前，开启定时自动续期
            this.renewExpire(key,hashKey);
            log.info("启动看门狗线程！");
        }
    }


    /**
     * 基于lua实现 释放锁
     * @param key 锁名称
     */
    public void luaUnLock(String key){
        //获取当前线程
        key = LOCK_PREFIX.concat(key);
        Long flag = redisTemplate.execute(UN_LOCK_SCRIPT, Collections.singletonList(key),getId());
        if(flag == null){
            throw new RuntimeException("该锁资源不属于你");
        }
        timer.cancel();
    }

    /**
     * 基于lua脚本 自动续期
     * @param key 锁名称
     */
    public void renewExpire(String key,String hashKey){
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                if(redisTemplate.execute(RENEW_EXPIRE_SCRIPT, Collections.singletonList(key), hashKey, String.valueOf(expireTime))){
                    //每30s续期一次
                    renewExpire(key,hashKey);
                }
            }
        }, this.expireTime * 1000 / 3);
    }

    //lua使用 拼接线程ID和UUID组成唯一标识
    private String getId() {
        return uuid + ":" + Thread.currentThread().getId();
    }
}
